package ru.compscicenter.uno.net;


import ru.compscicenter.uno.log.Logger;
import ru.compscicenter.uno.controller.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.*;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.Queue;

import ru.compscicenter.uno.model.Card;
import ru.compscicenter.uno.model.ModelException;
import ru.compscicenter.uno.model.StringRepresentation;
import ru.compscicenter.uno.wizard.Panel2Client;

public class TCPServer extends Thread {
    public static int CHUNK_SIZE = 10;
    private int port = 3128;
    private ServerSocket serverSocket;
    private ArrayList<Socket> sockets;
    private ArrayList<String> names;
    private ArrayList<Boolean> isActive;

    private AController controller;
    private boolean isStopped = false;
    private LinkedList<String> queries;
    private boolean toStop = false;

    public TCPServer(AController controller) {
        while (serverSocket == null) {
            try {
                toStop = false;
                port += 1;
                serverSocket = new ServerSocket(port);
                serverSocket.setSoTimeout(2000);
                serverSocket.setReuseAddress(false);
            } catch (IOException e) {
                Logger.write("TCPServer trying other port: " + port , Logger.Mode.ERROR);
            }
        }
        sockets = new ArrayList<Socket>();
        names = new ArrayList<String>();
        queries = new LinkedList<String>();
        isActive = new ArrayList<Boolean>();
        this.controller = controller;
    }

    public void setSockets(ArrayList<Socket> sockets) {
        this.sockets = sockets;
    }

    public void setNames(ArrayList<String> names) {
        this.names = names;
    }

    public int getPort() {
        return port;
    }

    public ArrayList<Socket> getSockets() {
        return sockets;
    }

    public ArrayList<String> getConnectedPlayersNames() {
        return names;
    }
    
    public void sendAllPlayersNames() throws IOException {
        String message = AController.NAMES + ":";// + controller.getPlayer(controller.getCurrentPlayerId()).getName();
        for (String name : names) {
            message += " " + name;
        }
        Logger.write("Sent names to all: " + message , Logger.Mode.ACTION);
        sendAll(message);
    }

    public void doWaitForPlayers() {
        Logger.write("Waiting for players", Logger.Mode.ACTION);
        int ID = 0;
        isStopped = false;
        while (!isStopped) {
            if (!controller.isReady) {
                try {
                    final Socket socket = serverSocket.accept();
                    socket.setSoTimeout(1000);
                    InputStream is = socket.getInputStream();
                    byte[] buff = new byte[2048];
                    int size = is.read(buff);
                    String data = new String(buff, 0, size);
                    //System.out.println(data);
                    if (data.startsWith(AController.CONNECT)) {
                        socket.setSoTimeout(0);
                        sockets.add(socket);
                        isActive.add(true);
                        Thread t = new Thread(new Runnable() {
                            @Override
                            public void run() {
                                int n = sockets.size() - 1;
                                while (isActive.get(n)) {
                                    try {
                                        String str = receive(socket);
                                        //tem.out.println(str);
                                        synchronized (queries) {
                                            if (str != null) {
                                                queries.push(str);
                                            }
                                        }
                                        try {
                                            Thread.sleep(10);
                                        } catch (InterruptedException e) {
                                            e.printStackTrace();
                                        }
                                    } catch (IOException e) {
                                        Logger.write(e.toString(), Logger.Mode.ERROR);
                                    }
                                }
                                isActive.remove(n);
                            }
                        });
                        t.setDaemon(true);
                        t.start();
                        String[] temp = data.split(" ");
                        Logger.write("Connected a player: " + temp[1], Logger.Mode.ACTION);
                        names.add(temp[1]);
                        send(socket, "Connected:" + StringRepresentation.getStringRepresentation(controller.getRules()));
                        System.out.println(StringRepresentation.getStringRepresentation(controller.getRules()));
                    }
                } catch (IOException e) {
                }
            } else {
                break;
            }
        }
    }

    public void startReceiveThreads() {
        while (!controller.isReady) {
            String str = receive();
            if (str != null) {
                queries.push(str);
            }
        }

        for (final Socket socket : sockets) {

        }
    }


    public void run() {
        //startReceiveThreads();
        while (true) {
            String data = null;

            while (queries.size() == 0) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            data = queries.get(0);
            queries.pop();
            //System.out.println(data);
            String[] dp = data.split(" ", 2);
            int from = Integer.parseInt(dp[0]);
            String[] dataParts = dp[1].split(" ");
            boolean success = false;



            try {
                if (dp[1].startsWith(AController.DISCONNECT)) {
                    Logger.write("Received a message to disconnect: " + dp[1], Logger.Mode.ACTION);

                    synchronized (isActive) {
                        sockets.remove(from);
                        names.remove(from);
                        isActive.set(from, false);
                    }
                    continue;
                } else if (dp[1].startsWith(AController.DO_DROP_CARD)) {
                    if (dataParts.length == 3) {
                        success = controller.getModel().doDropCard(Integer.parseInt(dataParts[1]), Integer.parseInt(dataParts[2]));
                        if (success) {
                            Logger.write("Player " + Integer.parseInt(dataParts[1]) + " dropped card: " + controller.getTopCard().toString(), Logger.Mode.ACTION);
                        }
                    } else {
                        success = controller.getModel().doDropCard(Integer.parseInt(dataParts[1]), Integer.parseInt(dataParts[2]), Card.Color.valueOf(dataParts[3]));
                        if (success) {
                            Logger.write("Player " + Integer.parseInt(dataParts[1]) + " dropped card: " + controller.getTopCard().toString() +
                                    " new color = " + controller.getTopColor().toString(), Logger.Mode.ACTION);
                        }
                    }
                } else if (dp[1].startsWith(AController.DO_TAKE_CARD)) {
                    success = controller.getModel().doTakeCard(Integer.parseInt(dataParts[1]));
                    if (success) {
                        Logger.write("Player " + Integer.parseInt(dataParts[1]) + " took a card", Logger.Mode.ACTION);
                    }
                } else if (dp[1].startsWith(AController.DO_SAY_UNO)) {
                    success = controller.getModel().doSayUno(Integer.parseInt(dataParts[1]));
                    if (success) {
                        Logger.write("Player " + Integer.parseInt(dataParts[1]) + " said UNO", Logger.Mode.ACTION);
                    }
                } else if (dp[1].startsWith(AController.DO_SAY_NO_UNO)) {
                    success = controller.getModel().doSomebodyDidntSayUno(Integer.parseInt(dataParts[1]), Integer.parseInt(dataParts[2]));
                    if (success) {
                        Logger.write("Player " + Integer.parseInt(dataParts[1]) + " said, that player " + Integer.parseInt(dataParts[2])
                                + "did not say UNO", Logger.Mode.ACTION);
                    }
                } else if (dp[1].startsWith(AController.DO_END_TURN)) {
                    success = controller.getModel().doEndTurn(Integer.parseInt(dataParts[1]));
                    if (success) {
                        Logger.write("Player " + Integer.parseInt(dataParts[1]) + " finished turn", Logger.Mode.ACTION);
                    }
                } else if (dp[1].startsWith(AController.DO_START_ROUND)) {
                    if (controller.isRoundEnded()) {
                        try {
                            send(sockets.get(from), AController.SUCCESS + " " + dp[1]);
                            controller.startRound();
                            continue;
                        } catch (IOException e) {
                            Logger.write("Failed to send a message: " +  AController.SUCCESS + " " + dp[1] + " " + e.toString(), Logger.Mode.ERROR);
                        }
                    }
                } else {
                    continue;
                }
            } catch (ModelException e) {
                //never thrown by controller
                Logger.write("Model exception come in TCPServer o_O : " + e, Logger.Mode.ERROR);
            }

            if (success) {
                try {
                    send(sockets.get(from), AController.SUCCESS + " " + dp[1]);
                } catch (IOException e) {
                    Logger.write("Failed to send a message: " +  AController.SUCCESS + " " + dp[1] + " " + e.toString(), Logger.Mode.ERROR);
                }
                for (int i = 0; i != sockets.size(); i++) {
                    if (i != from) {
                        try {
                            send(sockets.get(i), dp[1]);
                        } catch (IOException e) {
                            Logger.write("Failed to send a message: " + dp[1] + " " + e.toString(), Logger.Mode.ERROR);
                        }
                    }
                }
                controller.setIsChanged(true);
            } else {
                try {
                    send(sockets.get(from), AController.ERROR);
                } catch (IOException e) {
                    Logger.write("Failed to send a message: " + AController.ERROR + " " + e.toString(), Logger.Mode.ERROR);
                }
            }
        }
    }

    public ServerSocket getServerSocket() {
        return serverSocket;
    }

    public void sendAll(String message) throws IOException {
        for (Socket socket : sockets) {
            send(socket, message);
        }
    }

    public void send(Socket socket, String message) throws IOException {
        OutputStream os;
        os = socket.getOutputStream();
        String length = message.length() + "";
        while (length.length() < TCPServer.CHUNK_SIZE) {
            length = "0" + length;
        }
        os.write((length).getBytes());
        os.flush();
        os.write(message.getBytes());
        os.flush();
    }

    public String receive()  {
        for (int i = 0; i < sockets.size(); i++){
            try {
                InputStream is = sockets.get(i).getInputStream();
                byte[] length = new byte[100];
                int l = is.read(length, 0, CHUNK_SIZE);
                int toRead = Integer.parseInt(new String(length, 0, l));
                byte[] buff = new byte[2048];

                int hasRead = 0;
                while(hasRead != toRead) {
                    int size = 0;
                    try {
                        size = is.read(buff, hasRead, toRead - hasRead);
                    } catch (SocketTimeoutException e) {
                        if (hasRead > 0 && hasRead < toRead) {
                            continue;
                        } else {
                            throw e;
                        }
                    }
                    hasRead += size;
                }
                String res = new String(buff, 0, toRead);
                return i + " " + res;
            }
            catch (NullPointerException e){
                Logger.write("TCPServer: Null pointer exception in receive: " + e.toString(), Logger.Mode.ERROR);
            }
            catch (IOException e) {
                Panel2Client.showMessage("Lost connection to one of the players. \nPlease, start this game again. Thank you!");
            }
        }
        return null;

    }
    
    public String receive(Socket socket) throws IOException {
        InputStream is = socket.getInputStream();
        byte[] length = new byte[100];
        int l = is.read(length, 0, CHUNK_SIZE);
        int toRead = Integer.parseInt(new String(length, 0, l));
        byte[] buff = new byte[2048];

        int hasRead = 0;
        while(hasRead != toRead) {
            int size = 0;
            try {
                size = is.read(buff, hasRead, toRead - hasRead);
            } catch (SocketTimeoutException e) {
                if (hasRead > 0 && hasRead < toRead) {
                    continue;
                } else {
                    throw e;
                }
            }
            hasRead += size;
        }
        String res =  new String(buff, 0, toRead);
        return sockets.indexOf(socket) + " " + res;
    }

    public static ArrayList<String> getNetworkInterface()  {
        Enumeration<NetworkInterface> ni = null;
        int i = 1;
        ArrayList<String> addresses = new ArrayList<String>();

        try {
            ni = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e) {
            Logger.write("Failed to get all network interfaces: " + e.toString(), Logger.Mode.ERROR);
        }

        while (ni != null && ni.hasMoreElements()) {
            NetworkInterface niElement = ni.nextElement();
            Enumeration<InetAddress> ia = niElement.getInetAddresses();
            while (ia.hasMoreElements()) {
                InetAddress address = ia.nextElement();
                if (address instanceof Inet4Address) {
                    addresses.add(address.toString().substring(1));
                }
            }
        }
        return addresses;
    }

    public void stopServer() {
        try {
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
        sockets = new ArrayList<Socket>();
        names = new ArrayList<String>();
        isStopped = true;
        toStop = true;
    }

    public void reStartServer() {
        serverSocket = null;
        while (serverSocket == null) {
            try {
                toStop = false;
                port += 1;
                serverSocket = new ServerSocket(port);
                serverSocket.setSoTimeout(2000);
                serverSocket.setReuseAddress(false);
            } catch (IOException e) {
                Logger.write("TCPServer trying other port: " + port , Logger.Mode.ERROR);
            }
        }
        sockets = new ArrayList<Socket>();
        names = new ArrayList<String>();
        queries = new LinkedList<String>();
        this.controller = controller;
        isStopped = false;
    }

    public void stopBroadcast() {
        try {
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
        toStop = true;
    }

    public boolean getIsStopped() {
        return isStopped;
    }
}